﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.Security;
using Medianamik.Core.Cache;
using Medianamik.Core.Interfaces;
using Medianamik.Core.Logging;
using Medianamik.Core.Mail;

namespace Medianamik.Core.Security
{
    /// <summary>
    /// Classe implémentant la gestion du Membership dans les noeuds de Médianamik
    /// Source: http://msdn2.microsoft.com/en-us/library/6tc47t75.aspx
    /// </summary>
    public sealed class MedianamikMembershipProvider : MembershipProvider
    {
        private static readonly byte[] _key =
            HexToByte("81B3886355EC2D69FF30B6F7E8FEB6CA2DFF2DD6AB63E742BE719C2B8161D70A");

        private static readonly byte[] _IV = HexToByte("DAD358ED548EA2FFF9EC5587A946430C");

        #region Membres

        private readonly ILogger _logger = new ObjectFactory<ILogger>().Get("MedianamikMembershipProviderLogger");
        private string _applicationName;
        private bool _enablePasswordReset;
        private bool _enablePasswordRetrieval;
        private MachineKeySection _machineKey;
        private int _maxInvalidPasswordAttempts;
        private int _minRequiredNonAlphanumericCharacters;
        private int _minRequiredPasswordLength;
        private int _passwordAttemptWindow;
        private MembershipPasswordFormat _passwordFormat;
        private string _passwordStrengthRegularExpression;
        private bool _requiresQuestionAndAnswer;
        private bool _requiresUniqueEmail;
        //private Guid _usersRepositoryNodeInstance = MedianamikNodeInstances.UserRepository;
        private Guid _usersTypeId = MedianamikTypes.MedianamikUser;

        #endregion

        #region Overrides

        /// <summary>
        /// Initialiser les données du web.config
        /// </summary>
        /// <param name="name"></param>
        /// <param name="config"></param>
        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
                throw new ArgumentNullException("config", "Unable to access config membership data");

            base.Initialize(name, config);

            _applicationName = GetConfigValue(config["applicationName"],
                                              HostingEnvironment.ApplicationVirtualPath);
            _maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5"));
            _passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10"));
            _minRequiredNonAlphanumericCharacters =
                Convert.ToInt32(GetConfigValue(config["minRequiredNonAlphanumericCharacters"], "1"));
            _minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "7"));
            _passwordStrengthRegularExpression =
                Convert.ToString(GetConfigValue(config["passwordStrengthRegularExpression"], ""));
            _enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "true"));
            _enablePasswordRetrieval = Convert.ToBoolean(GetConfigValue(config["enablePasswordRetrieval"], "true"));
            _requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "false"));
            _requiresUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "true"));

            string temp_format = config["passwordFormat"].ToLowerInvariant();
            if (temp_format == null)
            {
                temp_format = "hashed";
            }

            switch (temp_format)
            {
                case "hashed":
                    _passwordFormat = MembershipPasswordFormat.Hashed;
                    break;
                case "encrypted":
                    _passwordFormat = MembershipPasswordFormat.Encrypted;
                    break;
                case "clear":
                    _passwordFormat = MembershipPasswordFormat.Clear;
                    break;
                default:
                    throw new ProviderException("Password format not supported.");
            }

            // Get encryption and decryption key information from the configuration.
            System.Configuration.Configuration cfg =
                WebConfigurationManager.OpenWebConfiguration(HostingEnvironment.ApplicationVirtualPath);
            _machineKey = (MachineKeySection)cfg.GetSection("system.web/machineKey");

            if (_machineKey.ValidationKey.Contains("AutoGenerate"))
                if (PasswordFormat == MembershipPasswordFormat.Hashed)
                    throw new ProviderException("Hashed passwords " +
                                                "are not supported with auto-generated keys.");
        }

        protected override byte[] EncryptPassword(byte[] password)
        {
            ICryptoTransform encryptor = new RijndaelManaged().CreateEncryptor(_key, _IV);
            using (var memoryStream = new MemoryStream())
            {
                using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                {
                    cryptoStream.Write(password, 0, password.Length);
                    cryptoStream.FlushFinalBlock();

                    byte[] cipherBytes = memoryStream.ToArray();

                    return cipherBytes;
                }
            }
        }

        protected override byte[] DecryptPassword(byte[] encodedPassword)
        {
            ICryptoTransform decryptor = new RijndaelManaged().CreateDecryptor(_key, _IV);
            using (var memoryStream = new MemoryStream(encodedPassword))
            {
                using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                {
                    var bytes = new byte[encodedPassword.Length];
                    int decryptedCount = cryptoStream.Read(bytes, 0, bytes.Length);

                    return bytes.Take(decryptedCount).ToArray();
                }
            }
        }

        #endregion

        #region Méthodes

        #region Privées

        /// <summary>
        /// Fonction utilitaire pour retourner une valueur du fichier de configuration
        /// </summary>
        /// <param name="configValue"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        private string GetConfigValue(string configValue, string defaultValue)
        {
            if (String.IsNullOrEmpty(configValue))
                return defaultValue;

            return configValue;
        }

        /// <summary>
        /// Encoder le mot de passe, le "hacher" ou le laisser en clair selon _passwordFormat
        /// </summary>
        /// <param name="password">Le mot de passe à encrypter</param>
        /// <returns>Le mot de passe encrypté</returns>
        private string EncodePassword(string password, MembershipPasswordFormat pPasswordFormat, string pSalt)
        {
            if (pPasswordFormat == MembershipPasswordFormat.Clear)
            {
                return password;
            }

            byte[] bytes = Encoding.Unicode.GetBytes(password);
            byte[] src = Convert.FromBase64String(pSalt);
            var dst = new byte[src.Length + bytes.Length];
            byte[] inArray = null;
            Buffer.BlockCopy(src, 0, dst, 0, src.Length);
            Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);

            if (pPasswordFormat == MembershipPasswordFormat.Hashed)
            {
                var hash = new HMACSHA1();
                hash.Key = HexToByte(_machineKey.ValidationKey);
                inArray = hash.ComputeHash(dst);
            }
            else
            {
                inArray = EncryptPassword(dst);
            }
            return Convert.ToBase64String(inArray);
        }


        /// <summary>
        /// Décrypter un mot de passe, selon le _passwordFormat
        /// </summary>
        /// <param name="encodedPassword">Mot de pas encrypté</param>
        /// <returns>Mot de passe décrypté</returns>
        private string UnEncodePassword(string encodedPassword, MembershipPasswordFormat pPasswordFormat, string pSalt)
        {
            if (pPasswordFormat == MembershipPasswordFormat.Clear)
            {
                return encodedPassword;
            }
            if (pPasswordFormat == MembershipPasswordFormat.Hashed)
            {
                throw new ProviderException("Cannot unencode a hashed password.");
            }

            // pPasswordFormat ==  MembershipPasswordFormat.Encrypted
            byte[] bytes = Convert.FromBase64String(encodedPassword);
            byte[] saltBytes = Convert.FromBase64String(pSalt);

            byte[] decrypted = DecryptPassword(bytes);

            return Encoding.Unicode.GetString(decrypted.Skip(saltBytes.Length).ToArray());
        }

        /// <summary>
        /// Covertir une chaîne hexadecimal en tableau de byte. Utilisé dans l'encryption des mots de passes.
        /// </summary>
        /// <param name="hexString">Chaîne hexadécimale</param>
        /// <returns>Tableau de byte</returns>
        private static byte[] HexToByte(string hexString)
        {
            var returnBytes = new byte[hexString.Length / 2];
            for (int i = 0; i < returnBytes.Length; i++)
                returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
            return returnBytes;
        }


        private MedianamikUser GetMembershipUserFromIUser(IUser pUser)
        {
            return new MedianamikUser("MedianamikMembershipProvider"
                                      , pUser.UserName
                                      , pUser.UserId
                                      , pUser.Email
                                      , pUser.PasswordQuestion
                                      , pUser.Comment
                                      , pUser.IsApproved
                                      , pUser.IsLockedOut
                                      , pUser.CreatedOn
                                      , pUser.LastLoginDate.GetValueOrDefault()
                                      , pUser.LastActivityDate.GetValueOrDefault()
                                      , pUser.LastPasswordChangedDate.GetValueOrDefault()
                                      , pUser.LastLockedOutDate.GetValueOrDefault());
        }

        #endregion

        #endregion

        public override string ApplicationName
        {
            get { return _applicationName; }
            set { _applicationName = value; }
        }

        public override bool EnablePasswordReset
        {
            get { return _enablePasswordReset; }
        }

        public override bool EnablePasswordRetrieval
        {
            get { return _enablePasswordRetrieval; }
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { return _maxInvalidPasswordAttempts; }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return _minRequiredNonAlphanumericCharacters; }
        }

        public override int MinRequiredPasswordLength
        {
            get { return _minRequiredPasswordLength; }
        }

        public override int PasswordAttemptWindow
        {
            get { return _passwordAttemptWindow; }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { return _passwordFormat; }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { return _passwordStrengthRegularExpression; }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { return _requiresQuestionAndAnswer; }
        }

        public override bool RequiresUniqueEmail
        {
            get { return _requiresUniqueEmail; }
        }

        /// <summary>
        /// Retourner un objet de type MedianamikUser
        /// </summary>
        /// <param name="providerUserKey"></param>
        /// <param name="userIsOnline"></param>
        /// <returns></returns>
        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            MedianamikUser mUser = null;
            if (providerUserKey is Guid)
            {
                IUser user = DALProviderManager.DALProvider.SecurityDALProvider.GetUser((Guid)providerUserKey);

                if (user != null)
                {
                    if (userIsOnline)
                    {
                        //Mettre à jour la date de dernière activité
                        DALProviderManager.DALProvider.SecurityDALProvider.SetUserLastActivityDate(
                            (Guid)providerUserKey, DateTime.Now);
                    }
                    mUser = GetMembershipUserFromIUser(user);
                }
            }
            return mUser;
        }

        /// <summary>
        /// Retourner un objet de type MedianamikUser
        /// </summary>
        /// <param name="username"></param>
        /// <param name="userIsOnline"></param>
        /// <returns></returns>
        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            IUser user = DALProviderManager.DALProvider.SecurityDALProvider.GetUser(username);
            MembershipUser mUser = null;
            if (user != null)
            {
                if (userIsOnline)
                {
                    //Mettre à jour la date de dernière activité
                    DALProviderManager.DALProvider.SecurityDALProvider.SetUserLastActivityDate(user.UserId, DateTime.Now);
                }
                mUser = GetMembershipUserFromIUser(user);
            }
            return mUser;
        }

        /// <summary>
        /// Mettre à jour une usager
        /// </summary>
        /// <param name="user"></param>
        public override void UpdateUser(MembershipUser user)
        {
            DALProviderManager.DALProvider.SecurityDALProvider.UpdateUser(user.Email, user.Comment, user.IsApproved,
                                                                          user.IsLockedOut, (Guid)user.ProviderUserKey);
        }

        /// <summary>
        /// Créer un usager
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <param name="email"></param>
        /// <param name="passwordQuestion"></param>
        /// <param name="passwordAnswer"></param>
        /// <param name="isApproved"></param>
        /// <param name="providerUserKey"></param>
        /// <param name="status"></param>
        /// <returns></returns>
        public override MembershipUser CreateUser(string username, string password, string email,
                                                  string passwordQuestion, string passwordAnswer, bool isApproved,
                                                  object providerUserKey, out MembershipCreateStatus status)
        {
            if (GetUser(username, false) != null)
            {
                throw new ProviderException("User already exists");
            }

            if ((providerUserKey != null) && !(providerUserKey is Guid))
            {
                status = MembershipCreateStatus.InvalidProviderUserKey;
                return null;
            }

            if (new PasswordValidator(this).ValidatePassword(password) != PasswordValidationResult.Success)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }


            string salt = GenerateSalt();
            IUser user = DALProviderManager.DALProvider.SecurityDALProvider.CreateUser(username,
                                                                                       EncodePassword(password,
                                                                                                      PasswordFormat,
                                                                                                      salt),
                                                                                       PasswordFormat, salt, email,
                                                                                       passwordQuestion, passwordAnswer,
                                                                                       isApproved,
                                                                                       (Guid?)providerUserKey ??
                                                                                       Guid.NewGuid());

            status = MembershipCreateStatus.Success;

            return GetMembershipUserFromIUser(user);
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            PasswordValidationResult validationResult = new PasswordValidator(this).ValidatePassword(newPassword);
            switch (validationResult)
            {
                case PasswordValidationResult.TooShort:
                    throw new ArgumentException("Password too short. Required password length: " +
                                                MinRequiredPasswordLength + ". Actual password length:" +
                                                newPassword.Length);
                case PasswordValidationResult.TooLittleNonAlphanumericCharacters:
                    throw new ArgumentException("Password needs at least " + MinRequiredNonAlphanumericCharacters +
                                                " non alphanumeric characters.");
                case PasswordValidationResult.DoesNotMatchRegularExpression:
                    throw new ArgumentException("Password does not match regular expression");
                case PasswordValidationResult.Success:
                default:
                    break;
            }

            IUser user = DALProviderManager.DALProvider.SecurityDALProvider.GetUser(username);
            if (user != null &&
                user.Password.Equals(EncodePassword(oldPassword, (MembershipPasswordFormat)user.PasswordFormat,
                                                    user.Salt)))
            {
                string newSalt = GenerateSalt();
                DALProviderManager.DALProvider.SecurityDALProvider.SetUserPassword(user.UserId,
                                                                                   EncodePassword(newPassword,
                                                                                                  PasswordFormat,
                                                                                                  newSalt),
                                                                                   PasswordFormat, newSalt);
                return true;
            }

            return false;
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password,
                                                             string newPasswordQuestion, string newPasswordAnswer)
        {
            IUser user = DALProviderManager.DALProvider.SecurityDALProvider.GetUser(username);
            if (user != null &&
                user.Password.Equals(EncodePassword(password, (MembershipPasswordFormat)user.PasswordFormat, user.Salt)))
            {
                DALProviderManager.DALProvider.SecurityDALProvider.SetUserPasswordQuestionAndAnswer(user.UserId,
                                                                                                    newPasswordQuestion,
                                                                                                    newPasswordAnswer);
                return true;
            }

            return false;
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            DALProviderManager.DALProvider.SecurityDALProvider.DeleteUser(username);
            return true;
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize,
                                                                  out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize,
                                                                 out int totalRecords)
        {
            var col = new MembershipUserCollection();
            List<MedianamikUser> users =
                DALProviderManager.DALProvider.SecurityDALProvider.GetUsersByName(usernameToMatch, pageIndex + 1,
                                                                                  pageSize, out totalRecords)
                    .Select(u => GetMembershipUserFromIUser(u)).ToList();

            users.ForEach(u => col.Add(u));

            return col;
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            var col = new MembershipUserCollection();
            List<MedianamikUser> users =
                DALProviderManager.DALProvider.SecurityDALProvider.GetUsers(pageIndex + 1, pageSize, out totalRecords)
                    .Select(u => GetMembershipUserFromIUser(u)).ToList();

            users.ForEach(u => col.Add(u));

            return col;
        }

        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException();
        }

        public override string GetPassword(string username, string answer)
        {
            if (!EnablePasswordRetrieval)
            {
                throw new NotSupportedException("Password Retrieval is disabled");
            }

            IUser user = DALProviderManager.DALProvider.SecurityDALProvider.GetUser(username);
            if (user != null)
            {
                if (user.PasswordFormat == (int)MembershipPasswordFormat.Hashed)
                {
                    throw new NotSupportedException("Hashed passwords cannot be retrieved");
                }
                if (RequiresQuestionAndAnswer
                    && user.PasswordAnswer.CompareTo(answer) > 0)
                {
                    throw new MembershipPasswordException("Answer is not valid");
                }

                return UnEncodePassword(user.Password, (MembershipPasswordFormat)user.PasswordFormat, user.Salt);
            }
            else
            {
                throw new ProviderException("Username not found");
            }
        }

        public override string GetUserNameByEmail(string email)
        {
            throw new NotImplementedException();
        }

        public override string ResetPassword(string username, string answer)
        {
            MembershipUser user = default(MembershipUser);
            if (string.IsNullOrEmpty(username)
                || (user = GetUser(username, false)) == null)
            {
                throw new ProviderException("Invalid Username");
            }

            string salt = GenerateSalt();
            string newPwd =
                Membership.GeneratePassword((MinRequiredPasswordLength < 14) ? 14 : MinRequiredPasswordLength,
                                            MinRequiredNonAlphanumericCharacters);
            string encodedPwd = EncodePassword(newPwd, PasswordFormat, salt);
            DALProviderManager.DALProvider.SecurityDALProvider.SetUserPassword((Guid)user.ProviderUserKey, encodedPwd,
                                                                               PasswordFormat, salt);

            return newPwd;
        }

        public override bool UnlockUser(string userName)
        {
            DALProviderManager.DALProvider.SecurityDALProvider.UnlockUser(userName);
            LockUserProfile(false, userName);
            var cacheKey = userName.ToLowerInvariant() + "FailedPasswordAttemptCount";
            CacheManager.Instance.Remove(cacheKey);

            return true;
        }

        /// <summary>
        /// Valider les usagers
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        public override bool ValidateUser(string username, string password)
        {
            IUser user = DALProviderManager.DALProvider.SecurityDALProvider.GetUser(username);

            if (user != null)
            {
                if (!user.IsApproved)
                {
                    return false;
                }
                if (user.IsLockedOut)
                {
                    return false;
                }
                bool isSuccessful =
                    user.Password.Equals(EncodePassword(password, (MembershipPasswordFormat)user.PasswordFormat,
                                                        user.Salt));
                ProcessPasswordAttemptResult(username, isSuccessful);
                return isSuccessful;
            }

            return false;
        }

        private void ProcessPasswordAttemptResult(string username, bool isSuccessful)
        {
            string cacheKey = username.ToLowerInvariant() + "FailedPasswordAttemptCount";
            TimeSpan FailedPasswordAttemptTimeSpan = TimeSpan.FromMinutes(_passwordAttemptWindow);
            if (isSuccessful)
            {
                CacheManager.Instance.Remove(cacheKey);
            }
            else
            {
                int failedAttempts = CacheManager.Instance.Get(cacheKey, FailedPasswordAttemptTimeSpan,
                                                               () => 0);
                CacheManager.Instance.Remove(cacheKey);
                failedAttempts = CacheManager.Instance.Get(cacheKey, FailedPasswordAttemptTimeSpan,
                                                           () => failedAttempts + 1);

                if (failedAttempts > MaxInvalidPasswordAttempts)
                {
                    DALProviderManager.DALProvider.SecurityDALProvider.LockUser(username);
                    _logger.Warn("User was locked: " + username);
                    LockUserProfile(true, username);

                    try
                    {
                        if (HttpContext.Current != null)
                        {
                            var mail = new MailManager();
                            mail.SendAccountLockedEmail(username, HttpContext.Current);
                        }
                    }
                    catch {}
                }
            }
        }

        private void LockUserProfile(bool locked, string  username)
        {
            var user = DALProviderManager.DALProvider.SecurityDALProvider.GetUser(username);
            var users = NodeManager.GetNodesEvenIfDeletedWithTypeSpecified(
                 new Param(MedianamikProperties.ProfileUser, user.UserId), "MedianamikProfile");

            if (users.Count > 0)
            {
                var userToLock = users.First();

                if (userToLock.IsActive)
                {
                    NodeManager.SetNodeActiveStatus(userToLock.NodeId, 
                        CultureInfo.InvariantCulture, !locked);
                }
            }
        }

        private string GenerateSalt()
        {
            var data = new byte[0x10];
            new RNGCryptoServiceProvider().GetBytes(data);
            return Convert.ToBase64String(data);
        }
    }
}